Skip to content

fix(server): centralize pre-validation hooks and attribute chain failures#938

Merged
bokelley merged 1 commit into
mainfrom
claude/issue-859-hook-diagnostics
Jun 7, 2026
Merged

fix(server): centralize pre-validation hooks and attribute chain failures#938
bokelley merged 1 commit into
mainfrom
claude/issue-859-hook-diagnostics

Conversation

@bokelley
Copy link
Copy Markdown
Contributor

@bokelley bokelley commented Jun 7, 2026

Summary

Diagnostics + test-quality follow-up to the pre-validation hook composition work that shipped in #860. The ordered-chain core (PreValidationHook / PreValidationHookChain / compose_pre_validation_hooks) already lives on main; this PR re-applies the remaining substance from @sangilish's draft #882 freshly onto current main (which moved substantially via #930/#933, rewriting serve.py / a2a_server.py / mcp_tools.py).

Changes

  • Single source of truth. Move the hook types, compose_pre_validation_hooks, and the _flatten / _apply helpers into a new adcp.server._hooks module. spec_compat.py re-imports and re-exports the public names (with __all__) so existing imports — including adcp.testing.decisioning's from adcp.server.spec_compat import PreValidationHooks — keep working. __init__.py, a2a_server.py, mcp_tools.py, and serve.py now import from _hooks.
  • Failure attribution. Add PreValidationHookError so INVALID_REQUEST names the failing chain index and callable, e.g. pre_validation_hook[1] second_hook raised RuntimeError: ... (and pre_validation_hook[0] bad_return returned list, expected dict) instead of a generic pre_validation_hook raised .... The dispatcher catches it on the shared create_tool_caller path, so MCP and A2A both surface the attributed message (A2A via the adcp_error envelope + mirrored TextPart).
  • Real-path tests. Replace the mock/spy MCPToolSet test with a real MCPToolSet.call_tool(...) behavior test; add an A2A ordered-chain executor test; add MCP + A2A chain-failure attribution tests asserting the index and callable appear in the error.

Import layering

_hooks.py depends only on the standard library, so it sits at the bottom of the adcp.server layering — every other server module may import from it freely, and it adds no generated_poc / _generated access. tests/test_import_layering.py passes.

Verification

  • tests/test_import_layering.py, tests/test_public_api.py — pass
  • tests/test_pre_validation_hooks.py, tests/test_a2a_server.py, tests/test_spec_compat_hooks.py (+ deprecation), unknown-field / schema-validation / serve-passthrough / testing-decisioning — pass
  • Full suite: 5613 passed, 38 skipped
  • make lint, make typecheck, make typecheck-all, make validate-generated — all green

Credit to @sangilish, whose draft #882 this re-applies; that PR can be closed as superseded.

Closes #859
Supersedes #882

🤖 Generated with Claude Code

…ures

Move the pre-validation hook types, compose_pre_validation_hooks, and the
_flatten/_apply helpers into a single source-of-truth module
adcp.server._hooks. spec_compat.py re-imports and re-exports the public
names with __all__ so existing imports keep working; __init__.py,
a2a_server.py, mcp_tools.py, and serve.py now import from _hooks.

Add PreValidationHookError so INVALID_REQUEST messages name the failing
chain index and callable (e.g. "pre_validation_hook[1] second_hook raised
RuntimeError: ...") instead of a generic "pre_validation_hook raised ...".
The dispatcher catches it on the shared create_tool_caller path, so both
MCP and A2A surface the attributed message.

Replace the mock/spy MCPToolSet test with a real call_tool() behavior test,
add an A2A ordered-chain executor test, and add MCP + A2A chain-failure
attribution tests asserting the index and callable appear in the error.

Closes #859
Supersedes #882

Co-Authored-By: sangilish <sangilish@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@aao-ipr-bot aao-ipr-bot Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clean refactor. Pulls the pre-validation hook machinery into one stdlib-only _hooks.py and lets it sit at the bottom of the server import layering — single source of truth, every transport reads the same code path.

Things I checked

  • _apply semantics are equivalent + better. _flatten_pre_validation_hooks is byte-identical to the original. New _apply_pre_validation_hooks (_hooks.py:73-99) wraps every hook exception and every non-dict return in PreValidationHookError instead of letting them escape raw / raising bare TypeError. Same INVALID_REQUEST code on the wire, strictly better message. Happy path unchanged.
  • Narrowing except Exceptionexcept PreValidationHookError (mcp_tools.py:2333-2342) is safe. _flatten (the only thing that raises TypeError for a non-callable chain) runs at mcp_tools.py:2327, outside the try and at caller-creation time, not on the dispatch path. Inside the try only _apply runs, and _apply now wraps everything into PreValidationHookError. Nothing else escapes.
  • No import breaks. spec_compat.py re-imports and re-exports the public names with an explicit __all__; Mapping/Sequence dropped (only the deleted funcs used them), Callable retained for spec_compat.py:175, Collection retained. The un-migrated consumer adcp.testing.decisioning (from adcp.server.spec_compat import PreValidationHooks) still resolves. _spec_compat_hooks_impl kept for test_spec_compat_hooks.py.
  • Import layering holds. _hooks.py imports only collections.abc / typing — no generated_poc / _generated access. test_import_layering.py governs the type layer, not this.
  • Tests exercise the real path. The mock/spy MCPToolSet test is gone, replaced by a real MCPToolSet.call_tool(...) chain test; the A2A test drives ADCPAgentExecutor.execute end-to-end and asserts the attributed message round-trips through both the adcp_error envelope and the mirrored TextPart. Assertions (pre_validation_hook[1], boom, RuntimeError, returned list, expected dict) match the PreValidationHookError format strings exactly.
  • Semver signal correct. Additive — new PreValidationHookError export, all prior names re-exported. fix(server): is the right shape; nothing on the public surface removed or re-typed.

Follow-ups (non-blocking — file as issues)

  • New public export PreValidationHookError reaches adcp.server.*. Check AGENTS.md / README.md for the hook-error surface so the docs don't drift behind the new attribution behavior.

Minor nits (non-blocking)

  1. Self-wrapping catch. _hooks.py:88 (except Exception) will re-wrap a PreValidationHookError into a nested raised PreValidationHookError: ... if an adopter hook ever raises the framework's own type. Harmless — no adopter has reason to — but the catch is a hair broader than except Exception minus the framework type.

LGTM. Follow-up noted.

@bokelley bokelley merged commit 3859131 into main Jun 7, 2026
26 checks passed
@bokelley bokelley deleted the claude/issue-859-hook-diagnostics branch June 7, 2026 12:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Server: support composing pre-validation hooks per tool

1 participant